Skip to content

fix(objectql): hook 条件对完整 record 求值 —— stored ⊕ payload,与 #4649 同源 (#4770) - #4786

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4770-hook-condition-merged-record
Aug 3, 2026
Merged

fix(objectql): hook 条件对完整 record 求值 —— stored ⊕ payload,与 #4649 同源 (#4770)#4786
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4770-hook-condition-merged-record

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4770

问题

声明式 hook 的 condition 一直是拿 ctx.input.data(只有本次改动的字段)求值的。ctx.previous 排在它后面,永远够不到,两者从不合并 —— 所以一个条件只能引用「本次恰好被改到」的字段;引用别的字段就 No such key,被 hook-wrappers.ts 的 catch 吞成 false,只留一条 warn。

对 guard 型 hook 这读作「放行」,对审计型 hook 读作「不记录」。showcase_audit_task_completion(record.done == true)因此在最常见的更新上根本不跑 —— 改个 status、改个 assignee,payload 里没有 done —— 而 showcase 每次启动刷 10 条 warn 就是它唯一的痕迹。

修法(本 PR 只做 issue 里的「修法 1」)

条件读到的 record 现在与 validation 谓词逐字同源(#1871 / #4649):

  • stored ⊕ payload —— 先铺 prior record,再盖本次 payload(payload 对它携带的字段仍然胜出);
  • 对「已声明字段」总全 —— 两边都没有的声明字段补 null,于是「表达式可不可求值」不再取决于 driver 存了哪些列;
  • 只覆盖已声明字段 —— 拼错的 / 未声明的 key 依旧不可求值、依旧被报出来,这是不可让步的一半。

materializeDeclaredFieldsvalidation/rule-validator.ts 提到 declared-fields.ts,两条路径共用同一个 helper —— record.done == true 不能因为「谁在求值」而有两种含义,这正是两边最容易漂移的地方。

物化只在持久状态确实在手时发生:insert(缺失就是真的没有值),或取到了 prior row 的 update。predicate(multi: true)批量更新拿不到 prior row,于是原样保留 payload,而不是补出一堆与 N 行存储状态相矛盾的 nullctx.ql.getObject() 是内存里的 registry 读取 —— 没有任何代码路径为此多读一行数据

明确不在本 PR 内(按 PM 分派范围)

「合并之后仍然求不出值」时的兜底(warn + 当作 false)一字未动。它对 guard 型和审计型 hook 的失败方向相反,属于公开契约层面的决定,由维护者单独定夺。

顺带修正的两处「declared ≠ delivered」

record 变总全之后,has(x) 不再是 null guard(声明字段哪怕值为 null 也是 present,null > null 照样炸)—— 与 #4649 学到的是同一课:

  • examples/app-showcase 的 over-budget 条件改用 != null;
  • skills/objectstack-data/references/data-hooks.md 里「用 has(record.x) 兜住未写入字段」的处方随之更正,并写清「条件描述的是记录的状态,不是本次 diff」;skills/objectstack-automation/SKILL.md 那句「hooks: the write payload」同步更新。

验证

pnpm dev -- --fresh(showcase,fresh 目录 = issue 里 rm -rf .objectstack 的等价物):

condition evaluation failed 条数
本分支 0
同一次构建、把改动 revert 掉的对照 10(与 issue 报告逐条一致:{"hook":"showcase_audit_task_completion","condition":"record.done == true","error":"No such key: done"})

回归测试:packages/objectql/src/hook-condition-merged-record.test.ts(12 例,含真引擎 + 只存已写列的 in-memory driver 的集成级 showcase 复现)。先验:把 hook-wrappers.ts 的改动 stash 掉后 12 例中 8 例失败。

 Test Files  107 passed (107)          # pnpm --filter @objectstack/objectql
      Tests  1685 passed (1685)
 Test Files  10 passed (10)            # packages/runtime src/sandbox(hook body / 嵌套写入)
      Tests  110 passed (110)
 Test Files  10 passed (10)            # @objectstack/example-showcase
      Tests  60 passed (60)

pnpm --filter @objectstack/objectql typecheck 干净;@objectstack/spec check:generated 8/8 up to date(改了 SKILL.md);check:skill-examples 202 例通过。

相关发现(未在本 PR 内修复)

🤖 Generated with Claude Code

https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ


Generated by Claude Code

…4770)

A declarative hook `condition` was evaluated against `ctx.input.data` — the
fields the current write happens to carry. `ctx.previous` sat behind it,
unreachable, and the two were never merged, so a condition could only reference
a field the update touched. Anything else aborted the CEL expression with
`No such key`, which the gate swallowed into `false` plus one WARN line.

For a guard-style hook that reads as "let it through"; for an audit-style hook
it reads as "do not record it". `showcase_audit_task_completion`
(`record.done == true`) therefore did NOT run on the most ordinary updates
there are — change the status, change the assignee — and the only trace was ten
warn lines per showcase boot.

The record a condition reads is now built exactly the way a validation
predicate's is (#1871 / #4649): stored ⊕ payload, made total over the object's
DECLARED fields, `null` when a declared key is in neither. `materializeDeclaredFields`
moves out of `validation/rule-validator.ts` into `declared-fields.ts` and is
shared by both paths, because `record.done == true` must not mean two different
things depending on which surface evaluates it.

Declared-only is the load-bearing half: a typo'd or undeclared key stays
unevaluable and is still reported. Materialisation runs only when the persisted
state is in hand — an insert, or an update whose prior row was fetched — so a
predicate bulk update keeps its payload rather than gaining nulls that
contradict N stored rows. `ctx.ql.getObject()` is an in-memory registry read;
no code path fetches a record it did not already load.

Out of scope, deliberately unchanged: what happens when a condition is STILL
unevaluable after merging (warn + treat as false). Its failure direction is
opposite for guard and audit hooks and is tracked separately.

`has(...)` is not a null guard once the record is total — the showcase's
over-budget condition is rewritten with `!= null`, and the hook docs that
prescribed `has(record.x)` are corrected with it.

Evidence: `pnpm dev -- --fresh` on this branch logs 0 `condition evaluation
failed` lines; the same boot with the fix reverted logs exactly the 10 the issue
reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 6:20am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 06:26
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 84b6e58 Aug 3, 2026
21 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4770-hook-condition-merged-record branch August 3, 2026 06:38
os-zhuang pushed a commit that referenced this pull request Aug 3, 2026
合并 #4768(C17)/ #4786 / #4780 / #4783(#4634,31 位能力位退役)后重生成。

按 #4535 §7 与「rebase/merge 静默回退」纪律处理三张 ratchet:
`dual-source-exports.baseline.json` / `authorable-surface.json` /
`json-schema.manifest.json`(+ `api-surface.json`)一律 `git checkout
origin/main --` 取 main 版本后全量重跑生成器,只重施本 PR 的一处改动。
其中 json-schema.manifest.json 与 authorable-surface.json 归 os-regen
merge driver 管、合并不产生冲突标记,最易静默回退,故以 gen:schema 实跑
输出为准。

逐项确认他人蓄意变更未被回滚(regen 后实测):
- #4783 `data/DriverCapabilities:*` 31 行 [RETIRED] + 3 行存活 —— 均在
- C10 `system/EnvironmentArtifact*` 删除 —— 仍为 0
- C17 `studio/ActionLocation` → `studio/ActionContributionLocation` —— 旧 0 新 1
- 本 PR `kernel/PackageDependency` → `kernel/ResolvedPackageDependency` —— 旧 0 新 1

`renamed-defs.ts` 冲突为两条独立改名条目并存(C17 与本簇),按时序保留两条。

dual-source 基线 2 → **0**(entries: []),#4535 第二批收官、双源账目归零。

Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants